package no.ntnu.item.arctis.runtime;

import java.util.HashSet;
import java.util.Random;
import java.util.Set;

public abstract class Runtime extends AbstractRuntime {

	private IRegister register;

	private IRuntimeTerminationListener terminationListener;

	public static interface IRuntimeTerminationListener {
		public void runtimeStopped();
	}

	public void setTerminationListener(IRuntimeTerminationListener terminationListener) {
		this.terminationListener = terminationListener;
	}

	public Runtime(IRegister register) {
		this.register = register;
		AbstractRuntime.setRuntime(this);

		Scheduler defaultScheduler = new Scheduler(this);
		IStateMachine defaultMachine = createDefaultStateMachine(defaultScheduler);
		defaultScheduler.add(defaultMachine);

		register.setDefaultMachine(defaultMachine);
		register.setDefaultScheduler(defaultScheduler);
	}
	
	public boolean isSessionActive(String sessionID) {
		return register.getStateMachineBySessionID(sessionID)!=null;
	}
	
	boolean isRunning = false;
	
	public boolean isRunning() {
		return isRunning;
	}

	public void start() {
		new Thread(register.getDefaultScheduler()).start();
		isRunning = true;
	}

	public void stop() {
		isRunning = false;
		// TODO stop all scheduler threads
		// TODO empty all queues
		if (terminationListener != null) {
			terminationListener.runtimeStopped();
			terminationListener = null;
		}
		register.getDefaultScheduler().destroy();
	}

	public void setContext(String key, Object value) {
		context.put(key, value);
	}

	void deliverInternalMessage(IStateMachine senderMachine, String sessionID, String signalID, Object data, String alias) {

		String sender = senderMachine.getSessionId();

		if (sessionID == null) {
			// send it to the main machine.
			if (register.getDefaultMachine() != null) {
				register.getDefaultMachine().getInputQueue().addInternalMessage(sender, sessionID, signalID, data);
				register.getDefaultScheduler().awakenScheduler();
			} else {
				Logger.getLogger().messageDroppedDuringRouting(sender, sessionID, signalID, "Default state machine is null.");
			}

			// IStateMachine stm = createSession(signalID, defaultScheduler);
			// if (stm != null) {
			// String newSessionID = getFreshSessionId();
			// add(stm, newSessionID);
			// machineToQueue.get(stm).addInternalMessage(sender, sessionID,
			// signalID, data);
			// } else {
			// Logger.getLogger().registerError("Internal message does not trigger session creation, but session ID was null",
			// sender, null, signalID);
			// }

		} else {
			IStateMachine queue = register.getStateMachineBySessionID(sessionID);
			if (queue != null) {
				queue.getInputQueue().addInternalMessage(sender, sessionID, signalID, data);
				register.getDefaultScheduler().awakenScheduler();
			} else {
				IStateMachine stm = createSession(signalID, register.getDefaultScheduler(), sessionID, senderMachine.sessionID);
				startStateMachine(stm);
				
				if(alias!=null) {
					stm.sessionAlias = alias;
					stm.registerAlias(alias, sessionID);
				}
				
				// TODO parent machine can be identified by the address
				if (stm != null) {
					Logger.getLogger().sessionCreated(stm.getClass().getSimpleName(), sessionID, stm.getSessionCount());
					register.registerStateMachine(sessionID, stm);
					register.getDefaultScheduler().add(stm);
					stm.getInputQueue().addInternalMessage(sender, sessionID, signalID, data);
					register.getDefaultScheduler().awakenScheduler();
//				// sending to the parent state machine is not useful. It will discard the signal 	
//				} else if (register.getDefaultMachine() != null) {
//					System.err.println("Did not find session with ID " + sessionID + " sending it to default machine.");
//					register.getDefaultMachine().getInputQueue().addInternalMessage(sender, sessionID, signalID, data);
//					register.getDefaultScheduler().awakenScheduler();
				} else {
					Logger.getLogger().messageDroppedDuringRouting(sender, sessionID, signalID,
							"No state machine was found for this signal. Default state machine is null, probably due to termination.");
				}
			}
		}
	}

	
	public void terminateStateMachine(IStateMachine stm) {
		
		
		//System.err.println("TERMINATING " + stm.getClass().getSimpleName());
		//((Register)register).printSessionIDToStateMachine();
		
		if(stm.childSessions.isEmpty()) {
			// this is how a perfect application should be. 
			Set<String> sessionsToRemove = new HashSet<String>();
			sessionsToRemove.add(stm.sessionID);
			removeChildSessionsAtAllParents(stm.parentSessionID, sessionsToRemove);
		} else {
			// this constitutes a flaw, we warn, and terminate all child machines. 
			
			Logger.getLogger().applicationError("Sessions terminated by surrounding session. State machine " + stm.getClass().getSimpleName() + " has still " + stm.childSessions.size() + " nested sessions.", null);
			
			// find all child session IDs
			Set<String> childSessionIDs = new HashSet<String>();
			childSessionIDs.add(stm.sessionID);
			collectAllChildSessions(childSessionIDs, stm);			
			
			// we terminate each child session
			for(String child: childSessionIDs) {
				childSessionIDs.add(child);
				IStateMachine c = register.getStateMachineBySessionID(child);
				if(c!=null) {
					register.removeStateMachine(c);
					Logger.getLogger().nestedSessionTerminated(c.getClass().getSimpleName(), c.sessionID, c.getSessionCount(), stm.getClass().getSimpleName(), stm.sessionID);
				}
			}
			
			removeChildSessionsAtAllParents(stm.parentSessionID, childSessionIDs);
		}
		
		
		register.removeStateMachine(stm);
		// The default state machine terminated, so the runtime will terminate
		// we assume that one runtime executes exactly one default machine.
		// session should be terminated, but this is the task of the application
		if (stm == register.getDefaultMachine()) {
			Logger.getLogger().systemTerminated();
			stop();
		} else {
			Logger.getLogger().sessionTerminated(stm.getClass().getSimpleName(), stm.sessionID, stm.getSessionCount());
		}
	}
	
	/**
	 * Moves down and collects all child sessions of this machine.
	 * 
	 * @param childSessionIDs
	 * @param stm
	 */
	private void collectAllChildSessions(Set<String> childSessionIDs, IStateMachine stm) {
		for(String child: stm.childSessions) {
			childSessionIDs.add(child);
			IStateMachine c = register.getStateMachineBySessionID(child);
			if(c!=null) {
				collectAllChildSessions(childSessionIDs, c);
			}
		}
	}
	
	/**
	 * Crawls up the hierarchy of sessions and removes the given session IDs from all of the state machines.
	 * @param parentSessionID
	 * @param sessionsToRemove
	 */
	private void removeChildSessionsAtAllParents(String parentSessionID, Set<String> sessionsToRemove) {
		boolean topReached = false;
		String currentParentSessionID = parentSessionID;
		while(!topReached) {
			if(currentParentSessionID==null) {
				IStateMachine p = register.getDefaultMachine();
				p.childSessions.removeAll(sessionsToRemove);
				topReached = true;
			} else {
				IStateMachine p = register.getStateMachineBySessionID(currentParentSessionID);
				if(p!=null) {
					p.childSessions.removeAll(sessionsToRemove);
					currentParentSessionID = p.parentSessionID;
				} else {
					System.err.println("State machine was not found " + currentParentSessionID);
					topReached = true;
				}
			}
		}
	}
	
	/**
	 * Add the session ID of stm to each of its parents, until the topmost state machine is reached.
	 * @param stm
	 */
	public void startStateMachine(IStateMachine stm) {
		boolean topReached = false;
		String currentParentSessionID = stm.parentSessionID;
		while(!topReached) {
			if(currentParentSessionID==null) {
				IStateMachine p = register.getDefaultMachine();
				p.childSessions.add(stm.sessionID);
				topReached = true;
			} else {
				IStateMachine p = register.getStateMachineBySessionID(currentParentSessionID);
				p.childSessions.add(stm.sessionID);
				currentParentSessionID = p.parentSessionID;
			}
		}
	}

	private final Random randomSessionIdGenerator = new Random();

	public String getFreshSessionId() {
		return "" + Math.abs(randomSessionIdGenerator.nextLong());
	}

	protected abstract boolean createsSession(String signalID);

	protected abstract IStateMachine createSession(String signalID, Scheduler scheduler, String sessionID, String parentSessionID);

	protected abstract IStateMachine createDefaultStateMachine(Scheduler scheduler);


	public void sendToBlock(String sessionID, String blockID, String signalID, Object data) {
		IStateMachine m;
		if(sessionID==null) {
			m = register.getDefaultMachine();
			if(m==null) {
				Logger.getLogger().applicationError("Default state machine is null, when trying to dispatch signal with ID " + signalID + "\n" + ((Register)register).printSessionIDToStateMachine(), null);
				return;
			}
		} else {
			m = register.getStateMachineBySessionID(sessionID);
			if(m==null) {
				Logger.getLogger().applicationError("Could not find a state machine for the session ID " + sessionID
					+ " while sending a signal with ID " + signalID + "\n" + ((Register)register).printSessionIDToStateMachine(), null);
				return;
			}
		}
		m.getInputQueue().addInternalMessage(null, null, signalID, data);
		// TODO get the responsible scheduler for this machine
		register.getDefaultScheduler().awakenScheduler();
	}

	public void sendToBlock(String sessionID, String blockID, String signalId) {
		sendToBlock(sessionID, blockID, signalId, null);
	}

	public void catchApplicationException(String explanation, Throwable t) {
		Logger.getLogger().applicationError(explanation, t);
	}

}